﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
#if !NET40
using CoNatural.Helpers;
#endif

namespace CoNatural.Data {
	/// <summary>
	/// Batch execution context
	/// </summary>
	public class BatchExecutionContext : ExecutionContextBase {
		/// <summary>
		/// Command item in batch
		/// </summary>
		public class BatchItem {
			/// <summary>
			/// Item 0-based index in batch
			/// </summary>
			public int Index { get; private set; }

			/// <summary>
			/// Item execution context
			/// </summary>
			public CommandExecutionContext Context { get; private set; }

			/// <summary>
			/// Item cached parameters
			/// </summary>
			public IEnumerable<Parameter> Parameters {
				get {
					foreach (var p in parameters.Values)
						yield return p;
				}
			}

			/// <summary>
			/// Gets item parameter by name
			/// </summary>
			/// <param name="name">The parameter name.</param>
			/// <returns>The parameter or null if name not found.</returns>
			public Parameter this[string name] {
				get {
					Parameter p = null;
					parameters.TryGetValue(name, out p);
					return p;
				}
			}

			private IDictionary<string, Parameter> parameters;
			internal BatchItem(int index, CommandExecutionContext ctx, IDbProvider dbProvider) {
				Index = index;
				Context = ctx;

				// cache command parameters
				parameters = new Dictionary<string, Parameter>();
				foreach (var p in Parameter.GetParameters(dbProvider, ctx.Command.GetType())) {
					p.BatchCommand = ctx.Command;
					parameters.Add(p.Name, p);
				}
			}
		}

		internal protected bool ReuseParameters { get; private set; }
		private IList<BatchItem> batch;
 
		/// <summary>
		/// Constructor for batch command execution
		/// </summary>
		internal BatchExecutionContext(IConnection connection, string name, bool reuseParameters) 
			: base(connection, null) {
 			CommandName = name;
			ReuseParameters = reuseParameters;
			batch = new List<BatchItem>();
		}

		public IEnumerable<BatchItem> GetItems() {
			foreach (var item in batch)
				yield return item;
		}

		#region Execution

		internal BatchItem AddCommand(CommandExecutionContext ctx) {
			var item = new BatchItem(batch.Count, ctx, Connection.DataBinder.DbProvider);
			batch.Add(item);
			return item;
		}

		internal void AddCommands<T>(IEnumerable<T> objs, Func<T, Tuple<object, ICommand>> tupleBinder) {
			// save command index by object to resolve references when linking output parameters
			var ixByObj = new Dictionary<object, int>();

			foreach (var o in objs) {
				var tuple = tupleBinder(o);
				if (tuple == null)
					continue;

				var obj = tuple.Item1;
				var iCommand = tuple.Item2;
				var ctx = new CommandExecutionContext(Connection, iCommand, obj);
				var item = AddCommand(ctx);

				// index by obj reference
				ixByObj[obj] = item.Index;

				// link indirectly mapped output parameters to guide the scripting engine
				foreach(var mapping in Connection.DataBinder.Map(obj, iCommand)) {
				   if (mapping.IndirectReference != null) {
						int refIx;
				      if (ixByObj.TryGetValue(mapping.IndirectReference, out refIx)) {
							Parameter link = batch[refIx][mapping.Target];
							if(link != null && link.Direction != ParameterDirection.Input) {
								Parameter p = item[mapping.Target];
								if(p != null)
									p.BatchLink = link;
							}
				      }
				   }
				}
			}
		}

		internal void Execute() {
			using (var reader = DbCommand.ExecuteReader()) {
				if (!reader.IsClosed) {
					// copy output parameter values
					int lastIndex = -1;
					while (reader.Read()) {
						lastIndex = BindOut(reader, lastIndex);
					}
					if (lastIndex >= 0)
						batch[lastIndex].Context.BindOut();
					reader.Close();
				}
			}
		}

		#endregion Execution

		#region Bindings

		/// <summary>
		/// Updates bound commands in batch with output parameter values
		/// </summary>
		/// <param name="reader">The data reader.</param>
		/// <param name="commands">The bound commands.</param>
		/// <param name="lastIndex">The last read index.</param>
		/// <returns>The current read index.</returns>
		private int BindOut(IDataReader reader, int lastIndex) {
			int commandIndex = reader.GetInt32(0);
			string parameterName = reader.GetString(1);
			object parameterValue = reader.GetValue(2);
			if (commandIndex != lastIndex) {
				if (lastIndex >= 0)
					batch[lastIndex].Context.BindOut();
				lastIndex = commandIndex;
			}
			// copy linked value
			batch[commandIndex][parameterName].Value = parameterValue;
			return lastIndex;
		}

		#endregion Bindings

		#region DbCommand Management

		internal protected override void OpenCommand() {
			Script = Connection.DataBinder.DbProvider.ScriptBatch(this);
		}

		protected override void CloseCommand() {}

		protected override void OpenDbCommand() {
			DbCommand = Connection.DataBinder.DbProvider.CreateCommand();

			// set properties
			DbCommand.CommandType = CommandType.Text;
			DbCommand.CommandText = Script;
			DbCommand.CommandTimeout = Connection.TimeOut;

			OpenDbConnection();
		}

		protected override void CloseDbCommand() {
			if (DbCommand != null)
				CloseDbConnection();
		}

		#endregion DbCommand Management
	}
}
